Skip to content

Plan history: show the latest finished slot, and stop the false 'stale' warning - #5414

Open
chalfontchubby wants to merge 4 commits into
mainfrom
fix/plan-history-latest-slot
Open

chalfontchubby wants to merge 4 commits into
mainfrom
fix/plan-history-latest-slot

Conversation

@chalfontchubby

@chalfontchubby chalfontchubby commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

Posted by Claude on behalf of @chalfontchubby.

Summary

The web page's Plan → History view had two problems:

  1. The most recent slots were missing. History is rebuilt from what actually happened, at most once an hour, while the live Plan view starts at the current slot. So the slots finished since the last rebuild appeared in neither view. For example, at 21:35 the History stopped at the 20:30 slot and the Plan started at 21:30, so 21:00–21:30 was nowhere.
  2. It said "Plan data is stale" most of the time. The page judged every view against the live plan's 15-minute limit. History is only rebuilt hourly, so the warning showed for about 45 minutes of every hour although nothing was wrong.

With this change History is rebuilt once per completed slot, so the last finished slot shows within one or two re-plans of ending. Each view is judged against how often it is actually republished, so the warning only appears when something has really stopped.

Detail

Rebuild cadence: calculate_yesterday()

  • It now rebuilds once per completed plan slot (plan_interval_minutes), on the first re-plan at least one run (5 minutes) after the slot ends. The wait is so that the boundary run's own predbat.cost_today write is already recorded when its history is read back. The key is history_slot(): a (date, slot number, UTC offset), stepped back one run in UTC. The offset keeps the hour that repeats when the clocks go back as two separate slots.
  • A change of local date also rebuilds straight away, so the forced midnight re-plan still moves cost_yesterday and savings_yesterday_* on to the day just ended at 00:00.
  • How often that is:
  • A rebuild takes 0.4–0.9 s on a live install.
  • The one-run wait uses RUN_EVERY (the run cadence), not PREDICT_STEP.

Stale warning: get_plan_renderer_js()

  • Each dataset the page reads carries refresh_minutes, the longest Predbat should take to republish it:
    • the live plan: calculate_plan_every, set only when the plan is published
    • History and "Yesterday without Predbat": a slot, plus the run it waits, plus a re-plan interval
  • The page warns a run (5 minutes) after that, never under 15 minutes. At the defaults that's Plan 15 minutes (as before) and History 50.
  • The check uses the data the current view shows. Before, the "unchanged" poll always checked the plan's timestamp. It hides the warning when the view has no data. The view-to-data mapping is now a single helper (currentViewData()), and the "no data yet" text no longer says "about once an hour".

Trade-offs, accepted

  • With calculate_plan_every at or above the slot length, re-plans can fall on slot boundaries, and the rebuild then happens on the boundary run.
  • Just after midnight the history is rebuilt twice: once on the 00:00 re-plan (new day), and again on the first re-plan after 00:05 (first slot of the new day).
  • cost_yesterday and savings_yesterday_predbat, which have large html/json attributes, are written twice as often.
  • The 1 am savings totals (savings_total_*) add the figures from the last rebuild before them. That is now about 00:40 at the defaults, not about 01:00, so late cloud load/PV data has about 40 minutes rather than an hour to land before it is counted.

Tests

  • test_calculate_yesterday:
    • early exit within a slot
    • no rebuild on the boundary run, then a rebuild on the next run (this test fails on main)
    • 15-minute slots and the midnight key
    • the repeated hour at the autumn clock change, under both pytz and zoneinfo (fails without the UTC offset in the key)
    • the midnight re-plan rebuilds, although its slot key still counts towards the day before
    • refresh_minutes on the published History and baseline data
  • Test doubles: the stubs returned "{}" (a string) where publish_html_plan and plan_write_debug return a dict. They now match the real functions.
  • New test_plan_staleness_js: structural checks on the page script (the per-view data, the limit arithmetic, hiding the warning without data, no remaining bare-timestamp checks). It fails on main's script.
  • Pre-commit and ./run_all --quick pass. The rendered page script passes node --check.

🤖 Generated with Claude Code

…ss by each view's own refresh interval

The plan page's History view is built by calculate_yesterday() from what actually happened, at most once every 59
minutes. The live Plan view starts at the current slot, so the slots finished since the last rebuild showed in
neither view: at 21:35 the History stopped at 20:30 and the Plan started at 21:30. And the page judged every view
against the live plan's 15-minute stale limit, so on the History view it warned "Plan data is stale" for about
45 minutes of every hour although nothing was wrong.

- calculate_yesterday() now rebuilds once per completed plan slot (plan_interval_minutes), on the first re-plan at
  least one run (5 minutes) after the slot ends, so the boundary run's own cost_today write is recorded before
  its history is read back. A rebuild takes 0.4-0.9 s on a live install; at the default 30-minute slots that is
  48 a day instead of 24 (before #2913 it ran on every re-plan, about 144).
- Each dataset the page shows carries refresh_minutes, the longest Predbat should take to republish it (the live
  plan: calculate_plan_every; the plan history: a slot, the run it waits and a re-plan interval). The page warns
  a run after that, never under 15 minutes, against the data the current view shows, and hides the warning when
  the view has no data. At the defaults: Plan 15 minutes (as before), History 50.

Trade-offs, kept: with calculate_plan_every at or above the slot length, re-plans can fall on slot boundaries
and the rebuild then happens there; cost_yesterday and savings_yesterday_* roll over at the first re-plan after
00:05 rather than at 00:00; cost_yesterday/savings_yesterday_predbat, with their large html/json attributes, are
written twice as often.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chalfontchubby chalfontchubby added the bug Something isn't working label Oct 5, 2026
history_slot() and history_refresh_minutes() wait one run past a slot boundary, and their docstrings
call that run PREDICT_STEP. One run is RUN_EVERY in const.py; PREDICT_STEP is the prediction step. Both
are 5 today, so nothing changes now, but if either changed the wait would quietly be wrong.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The slot key can skip the repeated daylight-saving hour, leaving History incomplete and falsely stale.

1 open finding
What changed in this PR

Updates plan history after each completed slot and applies view-specific stale-data thresholds, refining the hourly cadence introduced by #2913.

Changes:

  • Rebuilds history per completed plan slot.
  • Adds refresh metadata and per-view stale checks.
  • Expands history and renderer test coverage.
File Description
apps/​predbat/​output.py Adds slot-based rebuilding and refresh metadata.
apps/​predbat/​web.py Generalizes stale-warning text.
apps/​predbat/​web_helper.py Implements per-view staleness handling.
apps/​predbat/​unit_test.py Registers the new renderer test.
apps/​predbat/​tests/​test_plan_staleness_js.py Tests stale-warning JavaScript structure.
apps/​predbat/​tests/​test_calculate_yesterday.py Tests slot cadence and refresh metadata.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/predbat/output.py Outdated
chalfontchubby and others added 2 commits October 11, 2026 12:32
…as separate slots

history_slot() keyed slots by local date and wall-clock slot only, so the
first and second 01:35 on the night the clocks go back collapsed to one key.
With an hourly re-plan the second run was skipped and the History view went
two hours without a rebuild, past its advertised refresh interval.

Add the UTC offset to the key, and step back the one run in UTC rather than
on the wall clock, so the step lands on the right side of the change (pytz
keeps the old offset on wall-clock arithmetic; zoneinfo drops fold).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one-run wait counts the 00:00 run towards the last slot of the day
before, so when the previous rebuild was already in that slot the forced
midnight re-plan skipped it. With an hourly re-plan, cost_yesterday and the
savings_yesterday sensors went on showing the day before yesterday until
01:00; main rebuilt at 00:00 because the date had changed.

Rebuild whenever the local date has changed as well as when the slot has.
Also correct a test comment that still called the schedule hourly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants